Game {
	var ticker;
	var <>delta_t = 0.04;    // 1/25
	var actions;
	var <window;
	var <>sprites;
	var <>movables;
	var <>obstacles;
	var <width;
	var <height;
	var <>counter;
	//	var counter;
	*new { | title, width, height, nsprites |
		^super.new.init( title, width, height );
	}
	init { | title, w, h |
		width = w;
		height = h;
		sprites = [];
		actions = Dictionary.new;
		movables = [];
		obstacles = [];
		ticker   = Task {
			inf.do{ | count |
				counter = count;
				actions.do{ | action |
					action.value( counter );
				};
				window.refresh();
				this.collide( counter );
				delta_t.wait;
			}
		};
		window = Window( title, Rect( 0, 0, w, h ) );
		window.drawHook = { sprites.do{ | s |
			var sprite = s[ 1 ];
			Pen.use{
				Pen.translate( sprite.x, sprite.y );
				if( sprite.scale.notNil, { Pen.scale( *sprite.scale ) } );
				if( sprite.rotate.notNil, { Pen.rotate( sprite.rotate ) } );
				Pen.image( sprite.img( counter ) );
			}
		} };
		window.front;
	}
	addSprite { | sprite, layer |
		sprites = sprites ++ [[ layer, sprite ]];
		sprites = sprites.sort{ | x, y | x[ 0 ] < y[ 0 ] };
		if( sprite.type == \movable, {
			movables = movables ++ [ [ sprite, Set.new ] ] } );
		if( sprite.type == \obstacle, { obstacles = obstacles ++ sprite } );
		obstacles = obstacles.sort{
			| a, b |
			or( a.x < b.x,
				and(
					a.x == b.x,
					a.y < b.y ) )
		}
	}
	remSprite { | sprite |
		sprites = sprites.reject{ | x | x[ 1 ] == sprite };
		movables = movables.reject{ | x | x[ 1 ] == sprite };
		obstacles = obstacles.reject{ | x | x == sprite };
	}
	refresh {
		window.refresh;
	}
	start {
		ticker.start;
	}
	stop {
		ticker.stop;
	}
	addAction { | sym, act |
		actions.put( sym, act );
	}
	remAction { | sym |
		actions.removeAt( sym );
	}
	collide { | count |
		var obs;
		var step;
		var interp = 1;
		movables.do{ | mov |
			if( ( mov[ 0 ].x - mov[ 0 ].lastX ).abs > mov[ 0 ].width, {
				interp = max( interp,
					( mov[ 0 ].x - mov[ 0 ].lastX ).abs / mov[ 0 ].width )
			} );
			if( ( mov[ 0 ].y - mov[ 0 ].lastY ).abs > mov[ 0 ].height, {
				interp = max( interp,
					( mov[ 0 ].y - mov[ 0 ].lastY).abs / mov[ 0 ].height )
			} );
		};
		interp = interp.ceil;
		interp.do{ | j |
			step = ( j + 1 ) * interp.reciprocal;
			movables.do{ | mov, i |
				// for each overlap - did we just leave?
				mov[ 1 ].do{ | ob |
					if( not( mov[ 0 ].overlap( ob, step ) ), {
						mov[ 0 ].leave( ob );
						mov[ 1 ].remove( ob ) } ) };
				obs = obstacles; /* .extractRange{
					| ob |
					case
					{ ( ob.x + ob.width) < min( mov[ 0 ].lastX, mov[ 0 ].x ) }
					{ -1 }
					{ ob.x >
						( max( mov[ 0 ].x, mov[ 0 ].lastX ) + mov[ 0 ].width ) }
					{ 1 }
					{ true } { 0 } }; */
				( obs ++
					( movables[ ( i + 1 ).. ].collect{ | x | x[ 0 ] } ) )
				.do{ | ob |
					if( mov[ 0 ] != ob, {
						if( mov[ 0 ].overlap( ob, step ), {
							// overlap, did we just enter?
							if( ( mov[ 1 ] & Set[ ob ] ).size == 0, {
								mov[ 0 ].enter( ob );
								mov[ 1 ].add( ob ) } ) } ) } ) } } }
	}
}

